#!/usr/bin/python
# coding:utf-8

from __future__ import absolute_import

import time
import sys
import os
import json
import threading
import traceback
import opentracing
from opentracing.ext import tags as opentracing_tags
from opentracing.propagation import SpanContextCorruptedException
import datetime

from .utils import gethostname,local_ip
from .span_context import SpanContext
from .constants import SAMPLED_FLAG, DEBUG_FLAG, APPTRACK_HOSTNAME_TAG_KEY, APPTRACK_IPV4_TAG_KEY, NOOP_SPAN_ID, APPTRACK_VERSION_TAG_KEY
from . import codecs
from .thrift import make_string_tag, make_tags, make_log
import slugify
import traceback
import StringIO
import six
from .logs import LogSpan, LOG_LEVEL_EXCEPTION

LOG_RING_DEFAULT = 'system_manager'

class Span(LogSpan):
    __slots__ = ['_tracer', '_context',
                 'operation_name', 'start_time', 'end_time',
                 'logs', 'tags', 'update_lock']

    def __init__(self, context, tracer, operation_name=None,
                 tags=None, start_time=None,log_ring = [],init_tag=True):
        super(Span, self).__init__(context=context, tracer=tracer)
        self.operation_name = operation_name
        self.start_time = start_time or datetime.datetime.utcnow()
        self.end_time = None
        self.update_lock = threading.Lock()
        self.ring = log_ring
        if init_tag:
            self.tags = [(opentracing_tags.ERROR,False),(APPTRACK_HOSTNAME_TAG_KEY,gethostname()),(APPTRACK_IPV4_TAG_KEY,local_ip())]
        else:
            self.tags = []
        if tags:
            for k, v in six.iteritems(tags):
                if not init_tag and k == APPTRACK_VERSION_TAG_KEY:
                    continue
                self.tags.append([k,v])
                
        if not self.ring:
            self.ring = [LOG_RING_DEFAULT]
                
    def start(self):
        self.tracer.report_span(self)

    def finish(self, finish_time=None,error=False,**kwargs):
        if not self.is_sampled():
            return

        self.end_time = finish_time or datetime.datetime.utcnow()
        if error:
            exc_type, exc_value, exc_traceback = sys.exc_info()
            Span._on_error(self, exc_type, exc_value, exc_traceback)
            self.set_tags(kwargs)
        self.tracer.finish_span(self)
        
    def set_operation_name(self, operation_name):
        self.operation_name = operation_name
        self.tracer.set_span_name(self.span_id,self.operation_name)
        return opentracing.Span.set_operation_name(self,operation_name)

    @property
    def context(self):
        """Provides access to the SpanContext associated with this Span.

        The SpanContext contains state that propagates from Span to Span in a
        larger trace.

        :return: returns the SpanContext associated with this Span.
        """
        return self._context

    @property
    def tracer(self):
        """Provides access to the Tracer that created this Span.

        :return: returns the Tracer that created this Span.
        """
        return self._tracer

    @property
    def trace_id(self):
        return self.context.trace_id

    @property
    def span_id(self):
        return self.context.span_id

    @property
    def parent_id(self):
        return self.context.parent_id

    @property
    def flags(self):
        return self.context.flags

    def is_sampled(self):
        return self.context.flags & SAMPLED_FLAG == SAMPLED_FLAG

    def is_debug(self):
        return self.context.flags & DEBUG_FLAG == DEBUG_FLAG

    def __repr__(self):
        print self.context.trace_id,self.context.span_id,self.context.parent_id,self.context.flags
        c = codecs.span_context_to_string(trace_id=self.context.trace_id,span_id=self.context.span_id,
            parent_id=self.context.parent_id, flags=self.context.flags)
        return '%s %s.%s' % (c, self.tracer.service_name, self.operation_name)


    def set_tag(self, key, value):
        with self.update_lock:
            if key == opentracing_tags.SAMPLING_PRIORITY:
                if value > 0:
                    self.context.flags |= SAMPLED_FLAG | DEBUG_FLAG
                else:
                    self.context.flags &= ~SAMPLED_FLAG
            else:
                tag = make_string_tag(
                    key=key,
                    value=str(value),
                    max_length=self.tracer.max_tag_value_length,
                )
                #self.tags.append(tag)
        self.tracer.reporter.put_tag(self,key,value)
        return self
        
    def set_tags(self,tags):
        for k,v in tags.items():
            self.tracer.reporter.put_tag(self,k,v)
        return self

    def log_kv(self, key_values, timestamp=None):
        default_event= self.operation_name if self.operation_name else 'other'
        event=key_values.pop(opentracing.span.logs.EVENT, default_event)
        message = key_values.pop(opentracing.span.logs.MESSAGE)
        self._tracer.reporter.put_log(self.span_id,event,message,rings=key_values.pop('ring'),**key_values)
        return self

    def set_baggage_item(self, key, value):
        prev_value = self.get_baggage_item(key=key)
        new_context = self.context.with_baggage_item(key=key, value=value)
        self._context = new_context
        logs = {
            'event': 'baggage',
            'key': key,
            'value': value,
            'message':'set span %s baggage item %s value %s'%(self.span_id,str(key),str(value)),
            'ring':self.ring[0:1]
        }
        if prev_value:
            logs['override'] = True
        self.log_kv(key_values=logs)
        self._tracer.reporter.update_context(self._context)
        return self

    def get_baggage_item(self, key):
        return self.context.baggage.get(key)

    def __enter__(self):
        """Invoked when span is used as a context manager.

        :return: returns the Span itself
        """
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Ends context manager and calls finish() on the span.

        If exception has occurred during execution, it is automatically added
        as a tag to the span.
        """
        Span._on_error(self, exc_type, exc_val, exc_tb)
        self.finish()
        
    @classmethod
    def _on_error(cls,span, exc_type, exc_val, exc_tb):
        if not span or not exc_type:
            return
        error_stack = cls.get_stack(LOG_LEVEL_EXCEPTION,exc_type, exc_val, exc_tb)
        span.set_tag(opentracing_tags.ERROR, True)
        span.log_kv({
            opentracing.span.logs.EVENT: LOG_LEVEL_EXCEPTION,
            opentracing.span.logs.MESSAGE: str(exc_val),
            opentracing.span.logs.ERROR_OBJECT: str(exc_val),
            opentracing.span.logs.ERROR_KIND: str(exc_type),
            opentracing.span.logs.STACK: error_stack,
            'ring':span.ring[0:1]
        })
       
    @staticmethod
    def get_stack(event,exc_type=None, exc_val=None, exc_tb=None):
        io_stack = StringIO.StringIO() 
        io_stack.write('--- Logging %s ---\n'%event)
        if exc_type:
            traceback.print_exception(exc_type, exc_val, exc_tb, None, io_stack)
        else:
            traceback.print_stack(file=io_stack)
        stack = io_stack.getvalue()
        io_stack.write('Call stack:\n')
        io_stack.close()
        return stack
        
    @property
    def is_noop(self):
        return False
        
    @staticmethod
    def is_noop_span_id(span_id):
        if span_id == None or span_id == NOOP_SPAN_ID:
            return True
        return False
        
    def _logger(self, log_ring, event, content,need_record_where=False):
        assert(type(log_ring) == list)
        key_values = {
            opentracing.span.logs.EVENT:event,
            opentracing.span.logs.MESSAGE:content,
            'ring':log_ring
            }
        # 下面的代码将获取对log_kv函数的调用者信息，已追加进日志记录中
        if need_record_where:
            key_values.update({opentracing.span.logs.STACK:self.get_stack(event)})
        self.log_kv(key_values)

def spanid_to_span(tracer, span_id, need_wait_events_collector=None):
    #如果是noop span id则返回不具任何操作的span
    if Span.is_noop_span_id(span_id):
        return tracer.noop_span
        
    trace_id=tracer.trace_id
    ctx = tracer.get_context(span_id)
    if ctx is None:
        ctx = SpanContext(trace_id=trace_id, span_id=span_id, parent_id=0)
    else:
        assert(str(ctx['trace_id']) == str(trace_id))
        ctx = SpanContext(trace_id=trace_id, span_id=span_id, parent_id=ctx.get('parent_id',None),flags=ctx['flags'],baggage=ctx['baggage'])
    return Span(ctx,tracer)